Skip to content

feat: multi-forge support — adapter pattern + Gitea#2842

Open
afonsojramos wants to merge 26 commits intomainfrom
multi-platform-support
Open

feat: multi-forge support — adapter pattern + Gitea#2842
afonsojramos wants to merge 26 commits intomainfrom
multi-platform-support

Conversation

@afonsojramos
Copy link
Copy Markdown
Member

@afonsojramos afonsojramos commented May 6, 2026

Summary

Pivots Gitify from GitHub-only to multi-forge by introducing a ForgeAdapter contract (src/renderer/utils/forges/) that all forge-specific code routes through, then ports the existing GitHub modules into forges/github/ and lands a Gitea adapter as the first non-GitHub citizen. Account.forge is a required field with a one-shot legacy-state migration; capabilities (mark-as-done, unsubscribe, GraphQL enrichment) are surfaced per-adapter so unsupported actions hide gracefully instead of silently no-op'ing.

CONTRIBUTING.md replaces the "no other forges" stance with a multi-forge policy and MAINTAINERS.md lists per-forge owners (Gitea: @bircni and myself). Lint is clean and 944/944 tests pass; Gitea PAT login verified end-to-end against gitea.com.

Note: Gitea's notification model is absurdly simple

Closes #2786

@github-actions github-actions Bot added the enhancement New feature or enhancement to existing functionality label May 6, 2026
@afonsojramos afonsojramos linked an issue May 6, 2026 that may be closed by this pull request
@afonsojramos
Copy link
Copy Markdown
Member Author

@setchy I think this is a good first implementation with two forges. Now, if we want to expand this, then the login flow needs a bit of a rework. I don't mind giving it a go, but I'd rather work on that after the core implementation is there.

afonsojramos and others added 20 commits May 6, 2026 16:02
Adds the first non-GitHub forge adapter under the new ForgeAdapter
contract: client (fetch-based, no Octokit), types, transform, and
adapter registration. Wires Gitea PAT login into the Login and Accounts
routes, exposes the Gitea platform icon, and gates unsupported actions
(mark-as-done, unsubscribe, GraphQL enrichment) via capability flags.

Adapted from work originally proposed in #2787.

Co-authored-by: bircni <75789103+bircni@users.noreply.github.com>
@setchy
Copy link
Copy Markdown
Member

setchy commented May 6, 2026

@setchy I think this is a good first implementation with two forges. Now, if we want to expand this, then the login flow needs a bit of a rework. I don't mind giving it a go, but I'd rather work on that after the core implementation is there.

Awesome work @afonsojramos - love the adapter pattern (i was thinking similar, borrowing from Renovates platform implementation).

I'm going to pull this branch down and run it locally.

We can do this in follow up PRs, but what are you initial thoughts about the reorganizing required for Settings, Accounts and Filters? My hunch is Accounts and Filters should be mostly OK, but some of the Settings config options are GitHub specific.

Also, do you think we should flag the non-GitHub forges as experimental for a period of time, or, just go for it!

@afonsojramos afonsojramos force-pushed the multi-platform-support branch from 37bf36f to 4d9e45f Compare May 6, 2026 14:03
Comment thread MAINTAINERS.md
Copy link
Copy Markdown
Member

@setchy setchy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we add a section into README.md that outlines our forge adapter support matrix? we could convey current support and future potential forges perhaps

@afonsojramos
Copy link
Copy Markdown
Member Author

afonsojramos commented May 6, 2026

what are your initial thoughts about the reorganizing required for Settings, Accounts and Filters?

Agree with your hunch.

  • Accounts: already adapter-driven after this PR: the “add new account” overlay iterates listAdapters(), dev-settings opens via getAdapter(account).getDeveloperSettingsUrl(account), and the scopes pill short-circuits for non-GitHub. Remaining GitHub-flavored bits (the scopes warning UI in particular) are inert for Gitea, which is fine for now.

  • Filters: structurally nothing to do: they operate on normalized GitifyNotification. The data is just narrower from Gitea today (we map all reasons to subscribed, owner type to User, etc.).

  • Settings: this is where it actually leaks. participating, detailedNotifications, markAsDoneOnOpen and markAsDoneOnUnsubscribe only mean something on GitHub. Three options as I see it:

  1. Keep them global, label as (GitHub) where relevant - smallest change.
  2. Hide them when no GitHub account is configured.
  3. Per-account overrides - I'd say it would be over-complicating the solution.
    I'd lean towards 1 for now and revisit when a third forge lands.

do you think we should flag the non-GitHub forges as experimental?

I’d say just go for it, given the simplicity of Gitea’s notification system. It’s nearly a strict subset of GitHub’s, so not much room to surprise people. Future forges with weirder shapes (GitLab todos, Bitbucket reviews) can land behind an experimental: true flag on the adapter; capability-gating should already be the natural place to thread it through.

Comment thread README.md
Comment thread README.md Outdated
Comment thread src/renderer/utils/auth/scopes.ts
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented May 6, 2026

Comment thread CONTRIBUTING.md
@setchy setchy mentioned this pull request May 6, 2026
} from '../../../types';

import { getCommit, getCommitComment } from '../../api/client';
import { getCommit, getCommitComment } from '../../forges/github/client';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still GitHub?

},

validateToken: (token: Token) =>
token.length >= 8 && token.length <= 512 && !/[\r\n]/.test(token),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gitea PATs are 40-char lowercase hex. An 8-char minimum will accept random secrets pasted by mistake.
I'd suggest ^[a-f0-9]{40}$

function migrateLegacyAuthState(auth: AuthState): AuthState {
return {
...auth,
accounts: auth.accounts.map((a) => ({ ...a, forge: a.forge ?? 'github' })),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?? only covers undefined/null. A persisted account with forge: '' or forge: 'GitHub' (case mismatch) will pass through

repository: transformRepository(raw, account),
account,
order: 0,
display: undefined as unknown as GitifyNotificationDisplay,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Casting undefined through unknown defeats the type.

id: String(user.id),
login: user.login,
name: user.name,
name: user.name as string | null,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RefreshAccountData.data.name is now typed; the cast is unnecessary and hides type drift

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or enhancement to existing functionality

Development

Successfully merging this pull request may close these issues.

docs: exploring multi git platform support Add compatibility with Gitea

3 participants